package com.google.code.guava.beans;

import static com.google.common.base.Preconditions.checkNotNull;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import com.google.code.guava.base.Exceptions;
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.MapMaker;

/**
 * Some {@link Introspector} routines, wrapped in a friendlier API and using a
 * more multi-threading friendly caching system.
 */
public final class Introspection{

    enum BeanInfoAccessor implements Function<ClassTuple, BeanInfo>{
        INSTANCE{

            public BeanInfo apply(final ClassTuple tuple){
                try{
                    return Introspector.getBeanInfo(tuple.getBaseClass(),
                        tuple.getStopClass());
                }catch(final IntrospectionException e){
                    throw Exceptions.rethrow(e,
                        "Error getting BeanInfo for base class: %s and stop class: %s",
                        tuple.getBaseClass(),
                        tuple.getStopClass());
                }
            }

        }
    }

    static final class ClassTuple{

        public static Object from(final Class<?> beanClass){
            return new ClassTuple(beanClass);
        }

        public static ClassTuple from(final Class<?> beanClass,
            final Class<?> stopClass){
            return new ClassTuple(beanClass, stopClass);
        }

        private final Class<?> baseClass;

        private final Class<?> stopClass;

        private ClassTuple(final Class<?> baseClass){
            this(baseClass, null);
        }

        private ClassTuple(final Class<?> baseClass, final Class<?> stopClass){
            this.baseClass =
                checkNotNull(baseClass, "Bean class may not be null");
            this.stopClass = stopClass == null ? Object.class : stopClass;
        }

        @Override
        public boolean equals(final Object obj){

            if(obj instanceof ClassTuple){

                final ClassTuple other = (ClassTuple) obj;
                return

                Objects.equal(baseClass, other.baseClass)
                && Objects.equal(stopClass, other.stopClass);

            }

            return false;
        }

        public Class<?> getBaseClass(){
            return baseClass;
        }

        public Class<?> getStopClass(){
            return stopClass;
        }

        @Override
        public int hashCode(){
            return Objects.hashCode(baseClass, stopClass);
        }

    }

    private static final Map<ClassTuple, BeanInfo> BEANINFO_CACHE =
        new MapMaker().expiration(20, TimeUnit.MINUTES)
        .initialCapacity(255)
        .makeComputingMap(BeanInfoAccessor.INSTANCE);

    /**
     * Find a {@link PropertyDescriptor} by name in a given {@link BeanInfo}
     * object.
     * 
     * @exception RuntimeException
     *                if no such property exists
     */
    public static PropertyDescriptor findDescriptor(final BeanInfo beanInfo,
        final String propertyName){
        for(final PropertyDescriptor propertyDescriptor :

            beanInfo.getPropertyDescriptors()){

            if(propertyDescriptor.getName()
                .equals(propertyName)) return propertyDescriptor;
        }
        throw Exceptions.create("Unknown property: %s in class %s",
            propertyName,
            beanInfo.getBeanDescriptor()
            .getBeanClass());
    }

    /**
     * Find a {@link PropertyDescriptor} by name in a given bean class.
     * 
     * @exception RuntimeException
     *                if no such property exists
     */
    public static PropertyDescriptor findDescriptor(final Class<?> beanClass,
        final String propertyName){
        return findDescriptor(getBeanInfo(beanClass), propertyName);
    }

    /**
     * Get a {@link BeanInfo} object from the supplied class. Wraps the
     * {@link Introspector#getBeanInfo(Class)} mechanism in a cache using a
     * {@link java.util.concurrent.ConcurrentMap ConcurrentMap}, so it should
     * perform better in a multi-threading scenario than the standard
     * {@link Introspector} routines.
     * 
     * @param beanClass
     * @return the BeanInfo object
     * @exception NullPointerException
     *                if the supplied class is null
     * @exception RuntimeException
     *                if introspection fails
     */
    public static BeanInfo getBeanInfo(final Class<?> beanClass){
        return Introspection.BEANINFO_CACHE.get(ClassTuple.from(beanClass));
    }

    /**
     * Get a {@link BeanInfo} object from the supplied base class and stop
     * class. Wraps the {@link Introspector#getBeanInfo(Class, Class)} mechanism
     * in a cache using a {@link java.util.concurrent.ConcurrentMap
     * ConcurrentMap}, so it should perform better in a multi-threading scenario
     * than the standard {@link Introspector} routines.
     * 
     * @param beanClass
     * @param stopClass
     * @return the BeanInfo object
     * @exception NullPointerException
     *                if one of the supplied classes is null
     * @exception RuntimeException
     *                if introspection fails
     */
    public static BeanInfo getBeanInfo(final Class<?> beanClass,
        final Class<?> stopClass){

        return Introspection.BEANINFO_CACHE.get(ClassTuple.from(
            checkNotNull(beanClass, "Bean Class may not be null"), //
            checkNotNull(stopClass, "Stop Class may not be null") //
        ));
    }

    /**
     * Get a {@link BeanInfo} object from a bean object.
     * 
     * @exception NullPointerException
     *                if the supplied bean is null
     * @exception RuntimeException
     *                if introspection fails
     */
    public static BeanInfo getBeanInfo(final Object bean){

        return getBeanInfo(checkNotNull(bean).getClass());
    }

    /**
     * Returns a List of all {@link PropertyDescriptor PropertyDescriptors}
     * associated with a {@link BeanInfo} Object.
     * 
     * @exception NullPointerException
     *                if the supplied {@link BeanInfo} is null
     * @exception RuntimeException
     *                if introspection fails
     */
    public static ImmutableList<PropertyDescriptor> listPropertyDescriptors(
        final BeanInfo beanInfo){
        return ImmutableList.copyOf(checkNotNull(beanInfo).getPropertyDescriptors());
    }

    /**
     * Returns a List of all {@link PropertyDescriptor PropertyDescriptors}
     * associated with a bean class.
     * 
     * @exception NullPointerException
     *                if the supplied class is null
     * @exception RuntimeException
     *                if introspection fails
     */
    public static ImmutableList<PropertyDescriptor> listPropertyDescriptors(
        final Class<?> beanClass){
        final BeanInfo beanInfo = getBeanInfo(checkNotNull(beanClass));
        return listPropertyDescriptors(beanInfo);
    }

    /**
     * Returns a List of all {@link PropertyDescriptor PropertyDescriptors}
     * associated with a bean Object.
     * 
     * @exception NullPointerException
     *                if the supplied bean is null
     * @exception RuntimeException
     *                if introspection fails
     */
    public static ImmutableList<PropertyDescriptor> listPropertyDescriptors(
        final Object bean){
        checkNotNull(bean);
        final BeanInfo beanInfo = getBeanInfo(bean);
        return listPropertyDescriptors(beanInfo);
    }

    private Introspection(){}

}
